feat(vtuber): add OpenAI-compatible adapter#1234
Conversation
Expose POST /v1/chat/completions (SSE) backed by the OAB agent, so any OpenAI-compatible character skin (AniCompanion, Open-LLM-VTuber, …) gets a real agent with zero client changes. messages[] is flattened into the agent prompt; the agent's streamed reply is re-emitted as OpenAI chat.completion.chunk deltas via a per-request channel.id registry drained by the /ws recv loop. Inline [emotion] tags pass through untouched. Tier 1 of RFC openabdev#1233. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NXngQyvmJwQPNYsiRUNU2m
Additional: Frontend WebSocket Client DemoAdded examples/vtuber-demo/index.html — a minimal WebSocket client reference implementation for VTuber skins. What it does:Connects to OAB Gateway via raw WebSocket (ws://host:8080/ws?token=) Usage:Open index.html in browser, configure WS URL and token, connect and chat. Note:Currently the gateway's handle_oab_connection does not forward GatewayEvents from WS clients to OAB core — backend update needed for full functionality. Code: |
Tier-1 complete ✅All validation items checked:
CI: all checks and smoke tests passing. Next: Tier-2Tier-2 RFC opened as #1235 — WebSocket side-channel ( |
This comment has been minimized.
This comment has been minimized.
Tier-2 adds GET /v1/vtuber/ws — a persistent WebSocket that pushes agent_state, emotion, and notification events derived from GatewayReply commands. VTuber skins connect once and receive real-time state updates (thinking/working/idle, tool usage, emotion tags) without polling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NXngQyvmJwQPNYsiRUNU2m
|
Tier-2 WebSocket event stream pushed (acd3e38). Pending: e2e testing with a live VTuber skin. |
This comment has been minimized.
This comment has been minimized.
Replace Vec<WsClient> with HashMap<u64, WsClient> and an AtomicU64 counter. The old Vec+index scheme broke when broadcast() called swap_remove on dead clients — surviving clients' stored indices became stale, routing subscribe/pong to the wrong connection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NXngQyvmJwQPNYsiRUNU2m
This comment has been minimized.
This comment has been minimized.
F2: Cap in-flight /v1/chat/completions at 32 by checking vtuber_pending size before accepting — returns 429 when full. F3: Emit SSE comment `: waiting for agent` after 10s of silence, giving clients an early signal before the 180s hard timeout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NXngQyvmJwQPNYsiRUNU2m
|
Review findings addressed:
|
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NXngQyvmJwQPNYsiRUNU2m
|
All findings resolved, clippy CI fixed in c1b9495. Ready for merge — pending e2e testing with a live VTuber skin. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| [gateway] | ||
| url = "ws://openab-gateway:8080/ws" | ||
| platform = "vtuber" | ||
| streaming = true | ||
| streaming_placeholder = false # required: avoids the "…" placeholder ambiguity |
There was a problem hiding this comment.
I think it's not required anymore
| command = "kiro-cli" | ||
| args = ["acp", "--trust-all-tools"] | ||
| working_dir = "/home/agent" |
|
|
||
| ## References | ||
|
|
||
| - [ADR: Custom Gateway](adr/custom-gateway.md) |
There was a problem hiding this comment.
let's remove this ref, we are deprecating custom gateway.
| ``` | ||
| Skin ──POST /v1/chat/completions (SSE)──▶ Gateway (:8080) ◀──WebSocket── OAB Pod | ||
| choices[].delta.content (incl. inline [emotion] tags) (OAB connects out) | ||
| ``` |
There was a problem hiding this comment.
update this, we are running single binary now.
| #[cfg(feature = "vtuber")] | ||
| vtuber: None, | ||
| #[cfg(feature = "vtuber")] | ||
| vtuber_pending: Arc::new(Mutex::new(HashMap::new())), | ||
| #[cfg(feature = "vtuber")] | ||
| vtuber_ws_clients: None, |
There was a problem hiding this comment.
depends on
we sill simplify this
This comment has been minimized.
This comment has been minimized.
# Conflicts: # crates/openab-gateway/src/adapters/line.rs # crates/openab-gateway/src/adapters/teams.rs
54a2b2b to
507b5d6
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
CHANGES REQUESTED What This PR DoesDesktop character apps (AniCompanion, Open-LLM-VTuber, ChatVRM) that speak OpenAI chat completions can now point at OAB and get a full ACP-backed agent (tool use, code, MCP, memory) with zero client changes. Tier-1 provides the OpenAI-compatible SSE endpoint; Tier-2 adds an optional WebSocket side-channel pushing structured agent-state, emotion, and notification events. How It WorksTier-1: Tier-2: Feature-gated under Findings
Finding Details🟡 F1: Missing in-flight request capIn previous iterations, Without this cap, a misbehaving or malicious client can spawn unlimited sessions, each holding an Suggested fix: Restore the in-flight cap: let pending = state.vtuber_pending.lock().await;
if pending.len() >= 32 {
return (StatusCode::TOO_MANY_REQUESTS, "too many in-flight requests").into_response();
}Or make it configurable via 🟡 F2: Timing side-channel in auth comparisonif provided != Some(expected.as_str()) { ... }
if token != Some(expected.as_str()) { ... }Standard use subtle::ConstantTimeEq;
let ok = provided
.map(|p| p.as_bytes().ct_eq(expected.as_bytes()).into())
.unwrap_or(false);
if !ok { return 401; }This is defense-in-depth — practical exploitability depends on network jitter, but it's standard practice for auth token comparison. Baseline Check
What's Good (🟢)
Addressing External Reviewer Feedback@smallgun01
ℹ️ Noted: The proposed demo uses a different protocol path (raw gateway |
What problem does this solve?
Desktop character apps such as AniCompanion, Open-LLM-VTuber, and ChatVRM already speak OpenAI chat completions, but they usually connect to a raw LLM. This PR lets those skins point at OpenAB instead, so the same UI gets ACP-backed tool use, code editing, memory, MCP, and existing OpenAB steering.
Closes #1233
Discord Discussion URL: https://discord.com/channels/1491295327620169908/1520790210320011274
Architecture
The VTuber adapter now runs inside the unified OpenAB binary:
No separate gateway process or adapter config block is required for VTuber in unified mode. Set
VTUBER_ENABLED=trueon the OpenAB process, and the unified HTTP listener exposes the OpenAI-compatible endpoint.Proposed Solution
Tier-1: OpenAI-Compatible SSE
POST /v1/chat/completionsstreams OpenAI-compatiblechat.completion.chunkevents.messages[]is forwarded to the configured ACP agent with no adapter-added steering.[emotion]tags pass through for existing skins that already parse and strip them before TTS.messages[].Tier-2: Optional WebSocket Side Channel
GET /v1/vtuber/wspushes structured UI events without affecting OpenAI compatibility.agent_state,emotion,tool_status, andnotification.subscribeto filter event categories andpingfor keepalive.Authorization: Bearer <VTUBER_AUTH_KEY>or?token=.Why this approach?
typediscriminator matches common avatar tooling patterns and keeps protocol integration simple.Validation
cargo check -p openab --features unifiedcargo test -p openab has_unified_platform_env_checks --features unifiedcargo test -p openab-gateway vtuber— 12 VTuber tests passedNotes
docs/vtuber.mdnow documents unified-mode setup only.docs/config-reference.mdlists the VTuber unified environment variables.src/main.rsfor unified OpenAB deployments.